#pragma region Licensing
/*
       LOGL::GLib -> LorgonJortle's OpenGL Game Library
       Copyright (C) 2011  Jesse 'LorgonJortle' Wilkerson

       This program is free software: you can redistribute it and/or modify
       it under the terms of the GNU General Public License as published by
       the Free Software Foundation, either version 3 of the License, or
       (at your option) any later version.

       This program is distributed in the hope that it will be useful,
       but WITHOUT ANY WARRANTY; without even the implied warranty of
       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
       GNU General Public License for more details.

       You should have received a copy of the GNU General Public License
       along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
#pragma endregion

#include "StaticModel.h"

namespace LOGL_GLib
{
     namespace Content
     {

          /******************************************************************************
           StaticModel::StaticModel();
                    Arguments:
                         void

                    Description:
                         Constructs a StaticModel.

                    Returns:
                         The StaticModel object.
          *******************************************************************************/
          StaticModel::StaticModel()
          {
               Name = "LOGL::GLib StaticModel";
               localFile = GL_TRUE;
               faces = 0;

               /* Reserve initial sizes. */
               vertices.reserve(100);
               texCoords.reserve(100);
               normals.reserve(100);

               /* Set the default scale. */
               Scale.X = 1.0f;
               Scale.Y = 1.0f;
               Scale.Z = 1.0f;
          }

          /******************************************************************************
           StaticModel::StaticModel(const std::string& _Name);
                    Arguments:
                         const std::string& _Name:
                              The name to assign the new object.

                    Description:
                         Constructs a StaticModel with the specified name.

                    Returns:
                         The StaticModel object.
          *******************************************************************************/
          StaticModel::StaticModel(const std::string& _Name)
          {
               Name = _Name;
          }

          /******************************************************************************
           StaticModel::~StaticModel();
                    Arguments:
                         void

                    Description:
                         Destructs the StaticModel and release any indebted resources.

                    Returns:
                         void
          *******************************************************************************/
          StaticModel::~StaticModel()
          {
               Destroy();
          }

          /******************************************************************************
           GLboolean StaticModel::Load(const std::string& _Filename);
                    Arguments:
                         const std::string& _Filename:
                              The 3D model to attempt to load.

                    Description:
                         Attempts to load the specified model data.

                    Returns:
                         Whether or not the specified model loaded properly.
          *******************************************************************************/
          GLboolean StaticModel::Load(const std::string& _Filename)
          {
               /* Get ride of any existing data. */
               Destroy();

               /* Save the filename and foldername. */
               filename = _Filename;
               foldername = filename.substr(0, filename.find_last_of("/\\")) + "\\";
               if(foldername == filename + "\\")
               {
                    foldername = "";
                    localFile = GL_TRUE;
               }
               else
                    localFile = GL_FALSE;

               try
               {
                    MaterialGroup defaultMat;
                    defaultMat.Mat.Name = "Default";
                    MatGroups.push_back(defaultMat);

                    /* Determine what type of file it is. */
                    if(_Filename.find(".obj") != std::string::npos)
                         LoadOBJ();
               }
               catch(...)
               {
                    Destroy();
                    return GL_FALSE;
               }

               return GL_TRUE;
          }

          /******************************************************************************
           GLvoid StaticModel::LoadOBJ(const std::String _Filename);
                    Arguments:
                         const std::String _Filename:
                              The 3D model to attempt to load.

                    Description:
                         Attempts to parse the specified OBJ file for model data.

                    Returns:
                         void
          *******************************************************************************/
          GLvoid StaticModel::LoadOBJ()
          {
               /* Declare some essential temp variables. */
               std::ifstream reader;
               std::string line;
               GLchar* token = 0;
               std::string subToken;
               GLint currentMat = 0;

               /* Dimensions of the model. */
               GLfloat maxHeight = 0.0f;
               GLfloat minHeight = 0.0f;
               GLfloat maxWidth = 0.0f;
               GLfloat minWidth = 0.0f;
               GLfloat maxDepth = 0.0f;
               GLfloat minDepth = 0.0f;

               /* Attempt to open the file. */
               reader.open(filename.c_str());
               if(!reader.is_open())
                    throw Exception("Failed to open model: " + filename);

               /* Read the entire file. */
               while(reader.good())
               {
                    /* Get the next uncommented line. */
                    do
                    {
                         /* Read in a new line. */
                         getline(reader, line);

                         /* Strip any trailing \r (for Linux/Mac). */
                         if (!line.empty() && line[line.size() - 1] == '\r')
                        line.erase(line.size() - 1);

                         /* If we've hit the end, break out of the loop. */
                         if(line == "")
                              break;

                    } while(line.at(0) == '#');

                    /* Check if we need to scan an MTL library. */
                    if(line.substr(0, 6) == "mtllib")
                    {
                         /* Discard the first token. */
                         token = strtok(&line.at(0), " ");

                         token = strtok(0, " ");
                         LoadMTL(token);
                    }

                    /* Check if we need to switch materials. */
                    else if(line.substr(0, 6) == "usemtl")
                    {
                         /* Discard the first token. */
                         token = strtok(&line.at(0), " ");

                         token = strtok(0, " ");
                         currentMat = EnableMaterial(token);
                         MatGroups.at(currentMat).Start = faces;
                    }

                    /* Texture coordinate declaration. */
                    else if(line.substr(0, 2) == "vt")
                    {
                         /* Discard the first token. */
                         token = strtok(&line.at(0), " ");

                         /* Add the new coordinate. */
                         token = strtok(0, " ");
                         texCoords.push_back(atof(token));
                         token = strtok(0, " ");
                         texCoords.push_back(atof(token));
                    }

                    /* Vertex normal declaration. */
                    else if(line.substr(0, 2) == "vn")
                    {
                         /* Discard the first token. */
                         token = strtok(&line.at(0), " ");

                         /* Add the new normal. */
                         token = strtok(0, " ");
                         normals.push_back(atof(token));
                         token = strtok(0, " ");
                         normals.push_back(atof(token));
                         token = strtok(0, " ");
                         normals.push_back(atof(token));
                    }

                    /* Vertex declaration. */
                    else if(line.substr(0, 1) == "v")
                    {
                         /* Discard the first token. */
                         token = strtok(&line.at(0), " ");

                         /* A temp variable to test for dimensions. */
                         GLfloat temp = 0.0f;

                         /* Add X. */
                         token = strtok(0, " ");
                         temp = atof(token);
                         vertices.push_back(temp);

                         /* Check if temp is outside the current dimensions. */
                         if(temp < minWidth)
                              minWidth = temp;
                         else if(temp > maxWidth)
                              maxWidth = temp;

                         /* Add Y. */
                         token = strtok(0, " ");
                         temp = atof(token);
                         vertices.push_back(temp);

                         /* Check if temp is outside the current dimensions. */
                         if(temp < minHeight)
                              minHeight = temp;
                         else if(temp > maxHeight)
                              maxHeight = temp;

                         /* Add Z. */
                         token = strtok(0, " ");
                         temp = atof(token);
                         vertices.push_back(temp);

                         /* Check if temp is outside the current dimensions. */
                         if(temp < minDepth)
                              minDepth = temp;
                         else if(temp > maxDepth)
                              maxDepth = temp;
                    }

                    /* Face declaration. */
                    else if(line.substr(0, 1) == "f")
                    {
                         /* Temporary face data. */
                         std::deque<Vertex3> face;

                         /* Discard the first token. */
                         token = strtok(&line.at(0), " ");

                         /* Parse the face. */
                         token = strtok(0, " ");
                         while(token != 0)
                         {
                              subToken = token;

                              /* Vertex//Normal grouping. */
                              if(subToken.find("//") != std::string::npos)
                              {
                                   GLshort slash = subToken.find_first_of("/");
                                   GLint vertex = atoi(subToken.substr(0, slash).c_str()) - 1;
                                   GLint normal = atoi(subToken.substr(slash + 2).c_str()) - 1;

                                   vertex *= 3;
                                   normal *= 3;

                                   Vertex3 vert;
                                   vert.Position.X = vertices.at(vertex);
                                   vert.Position.Y = vertices.at(vertex + 1);
                                   vert.Position.Z = vertices.at(vertex + 2);

                                   vert.TexCoord.X = 0.0f;
                                   vert.TexCoord.Y = 0.0f;

                                   vert.Normal.X = normals.at(normal);
                                   vert.Normal.Y = normals.at(normal + 1);
                                   vert.Normal.Z = normals.at(normal + 2);
                                   face.push_back(vert);
                              }
                              /* Vertex/Texture grouping. */
                              else if(subToken.find("/") != std::string::npos)
                              {
                                   GLshort slash = subToken.find_first_of("/");
                                   GLint vertex = atoi(subToken.substr(0, slash).c_str()) - 1;
                                   GLint texCoord = atoi(subToken.substr(slash + 1).c_str()) - 1;

                                   vertex *= 3;
                                   texCoord *= 2;

                                   Vertex3 vert;
                                   vert.Position.X = vertices.at(vertex);
                                   vert.Position.Y = vertices.at(vertex + 1);
                                   vert.Position.Z = vertices.at(vertex + 2);

                                   vert.TexCoord.X = texCoords.at(texCoord);
                                   vert.TexCoord.Y = texCoords.at(texCoord + 1);

                                   vert.Normal.X = 0.0f;
                                   vert.Normal.Y = 0.0f;
                                   vert.Normal.Z = 0.0f;

                                   face.push_back(vert);
                              }
                              /* Vertex. */
                              else
                              {
                                   GLint vertex = atoi(subToken.c_str()) - 1;

                                   vertex *= 3;

                                   Vertex3 vert;
                                   vert.Position.X = vertices.at(vertex);
                                   vert.Position.Y = vertices.at(vertex + 1);
                                   vert.Position.Z = vertices.at(vertex + 2);

                                   vert.TexCoord.X = 0.0f;
                                   vert.TexCoord.Y = 0.0f;

                                   vert.Normal.X = 0.0f;
                                   vert.Normal.Y = 0.0f;
                                   vert.Normal.Z = 0.0f;

                                   face.push_back(vert);
                              }

                              /* Next group. */
                              token = strtok(0, " ");
                         }

                         /* Triangulate the face. */
                         if(face.size() > 3)
                         {
                              for(GLint i = 0; i < face.size() - 2; i++)
                              {
                                   /* Add the base vert. */
                                   Data.push_back(face.at(0));

                                   /* Add the other two verts (to make a triangle). */
                                   Data.push_back(face.at(i + 1));
                                   Data.push_back(face.at(i + 2));

                                   MatGroups.at(currentMat).Faces++;
                                   faces++;
                              }
                         }
                         else
                         {
                              /* It's already a triangle. Add the vertices. */
                              Data.push_back(face.at(0));
                              Data.push_back(face.at(1));
                              Data.push_back(face.at(2));

                              MatGroups.at(currentMat).Faces++;
                              faces++;
                         }
                    }
               }

               /* Close our file. */
               reader.close();

               /* Report the max/min dimensions that we found. */
               Center.X = maxWidth - ((maxWidth - minWidth) / 2);
               Center.Y = maxHeight - ((maxHeight - minHeight) / 2);
               Center.Z = maxDepth - ((maxDepth - minDepth) / 2);

               TopLeft.X = minWidth;
               TopLeft.Y = maxHeight;
               TopLeft.Z = maxDepth;

               BottomRight.X = maxWidth;
               BottomRight.Y = minHeight;
               BottomRight.Z = minDepth;

               /* Upload all data to the card. */
               if(!UploadData())
                    throw Exception("Failed to upload model: " + Name);
          }

          /******************************************************************************
           GLvoid StaticModel::LoadMTL(const std::string& _Filename);
                    Arguments:
                         const std::string& _Filename:
                              The MTL file to parse.

                    Description:
                         Attempts to parse the specified MTL file for material data.

                    Returns:
                         void
          *******************************************************************************/
          GLvoid StaticModel::LoadMTL(const std::string& _Filename)
          {
               /* Declare essential variables. */
               std::ifstream reader;
               std::string line;
               GLchar* token = 0;
               GLint currentMat = 0;

               /* Try to open the material file. */
               reader.open(foldername.empty() ? _Filename.c_str() : (foldername + "\\" + _Filename).c_str());


               /* If we can't we'll just return to the OBJ loading. */
               if(!reader.is_open())
                    return;

               while(reader.good())
               {
                    /* Get the next uncommented line. */
                    do
                    {
                         /* Read in a new line. */
                         getline(reader, line);

                         /* Strip any trailing \r (for Linux/Mac). */
                         if (!line.empty() && line[line.size() - 1] == '\r')
                        line.erase(line.size() - 1);

                         /* If we've hit the end, break out of the loop. */
                         if(line == "")
                              break;

                    } while(line.at(0) == '#' && reader.good());

                    /* New material. */
                    if(line.substr(0, 6) == "newmtl")
                    {
                         /* Discard the first token */
                         token = strtok(&line.at(0), " ");

                         /* Create the new material. */
                         token = strtok(0, " ");
                         MaterialGroup newMat;
                         newMat.Mat.Name = token;
                         MatGroups.push_back(newMat);
                         currentMat = MatGroups.size() - 1;
                    }

                    /* Ambient color. */
                    if(line.substr(0, 2) == "Ka")
                    {
                         /* Discard the first token */
                         token = strtok(&line.at(0), " ");

                         /* Set the specified ambiance. */
                         token = strtok(0, " ");
                         MatGroups.at(currentMat).Mat.Ambient.X = atof(token);
                         token = strtok(0, " ");
                         MatGroups.at(currentMat).Mat.Ambient.Y = atof(token);
                         token = strtok(0, " ");
                         MatGroups.at(currentMat).Mat.Ambient.Z = atof(token);
                    }

                    /* Diffuse color. */
                    else if(line.substr(0, 2) == "Kd")
                    {
                         /* Discard the first token */
                         token = strtok(&line.at(0), " ");

                         /* Set the specified diffuse. */
                         token = strtok(0, " ");
                         MatGroups.at(currentMat).Mat.Diffuse.X = atof(token);
                         token = strtok(0, " ");
                         MatGroups.at(currentMat).Mat.Diffuse.Y = atof(token);
                         token = strtok(0, " ");
                         MatGroups.at(currentMat).Mat.Diffuse.Z = atof(token);
                    }

                    /* Specular color. */
                    else if(line.substr(0, 2) == "Ks")
                    {
                         /* Discard the first token */
                         token = strtok(&line.at(0), " ");

                         /* Set the specified specular. */
                         token = strtok(0, " ");
                         MatGroups.at(currentMat).Mat.Specular.X = atof(token);
                         token = strtok(0, " ");
                         MatGroups.at(currentMat).Mat.Specular.Y = atof(token);
                         token = strtok(0, " ");
                         MatGroups.at(currentMat).Mat.Specular.Z = atof(token);
                    }

                    /* Shininess weight. */
                    else if(line.substr(0, 2) == "Ns")
                    {
                         /* Discard the first token */
                         token = strtok(&line.at(0), " ");

                         /* Set the specified shininess. */
                         token = strtok(0, " ");
                         MatGroups.at(currentMat).Mat.Shininess = atof(token);
                         MatGroups.at(currentMat).Mat.Shininess /= 1000.0f;
                         MatGroups.at(currentMat).Mat.Shininess *= 128.0f;
                    }

                    /* Opacity. */
                    else if(line.substr(0, 2) == "Tr" || line.substr(0, 1) == "d")
                    {
                         /* Discard the first token */
                         token = strtok(&line.at(0), " ");

                         /* Set the specified shininess. */
                         token = strtok(0, " ");
                         MatGroups.at(currentMat).Mat.Alpha = atof(token);
                    }

                    /* Texture map. */
                    else if(line.substr(0, 6) == "map_Kd")
                    {
                         /* Discard the first token */
                         token = strtok(&line.at(0), " ");

                         /* Load the texture. */
                         token = strtok(0, " ");
                         if(token != 0)
                              MatGroups.at(currentMat).Mat.Texture.Load(localFile ? token : foldername + token);
                    }
               }

               /* Close our file stream. */
               reader.close();
          }

          /******************************************************************************
           GLvoid StaticModel::EnableMaterial(const std::string& _MaterialName);
                    Arguments:
                         const std::string& _MaterialName:
                              The name of the material to search for, and enable if found.

                    Description:
                         Attempts to enable the specified material.

                    Returns:
                         void
          *******************************************************************************/
          GLint StaticModel::EnableMaterial(const std::string& _MaterialName)
          {
               /* Iterate through all material groups. */
               for(std::deque<MaterialGroup>::iterator i = MatGroups.begin(); i < MatGroups.end(); i++)
               {
                    /* See if they are the same materials. */
                    if(i->Mat.Name == _MaterialName)
                         return i - MatGroups.begin();
               }

               /* If we didn't find it, just return the default material. */
               return 0;
          }

          /******************************************************************************
           GLboolean StaticModel::UploadData();
                    Arguments:
                         void

                    Description:
                         Uploads the model's data to the openGL server.

                    Returns:
                         Whether or not the data upload was successful.
          *******************************************************************************/
          GLboolean StaticModel::UploadData()
          {
               /* Calculate some statistics. */
               faces = (vertices.size() / 3) / 3;
               vertexCount = vertices.size() / 3;
               texCoordCount = texCoords.size() / 2;
               normalCount = normals.size() / 3;

               /* Only bother uploading if we have geometric data. */
               if(vertices.size() > 0)
               {
                    /* Delete any existing buffer. */
                    if(glIsBuffer(VBO))
                         glDeleteBuffers(1, &VBO);

                    /* Generate a new buffer. */
                    glGenBuffers(1, &VBO);
                    glBindBuffer(GL_ARRAY_BUFFER, VBO);
                    glBufferData(GL_ARRAY_BUFFER, Data.size() * sizeof(Vertex3), &Data.at(0), GL_STATIC_DRAW);
               }
               else
               {
                    /* We don't have any geometric data. Let's leave. */
                    Data.clear();
                    return GL_FALSE;
               }

               /* Clear the temp data. */
               std::vector<GLfloat> temp;
               vertices.clear();
               temp.swap(vertices);
               texCoords.clear();
               temp.swap(texCoords);
               normals.clear();
               temp.swap(normals);

               /* Success! */
               return GL_TRUE;
          }

          /******************************************************************************
           GLvoid StaticModel::Destroy();
                    Arguments:
                         void

                    Description:
                         Destroys the model, releasing any indebted resources.

                    Returns:
                         void
          *******************************************************************************/
          GLvoid StaticModel::Destroy()
          {
               filename = "";
               MatGroups.clear();
               Data.clear();
               std::vector<Vertex3>().swap(Data);

               if(glIsBuffer(VBO))
                    glDeleteBuffers(1, &VBO);
          }

          /******************************************************************************
           GLvoid StaticModel::Draw();
                    Arguments:
                         void

                    Description:
                         Renders the model's VBO if it has been uploaded.

                    Returns:
                         void
          *******************************************************************************/
          GLvoid StaticModel::Draw()
          {
               /* Only draw if we've uploaded. */
               if(Data.size() > 0)
               {
                    /* Bind our VBO and setup vertex drawing. */
                    glBindBuffer(GL_ARRAY_BUFFER, VBO);
                    glVertexAttribPointer(Attribute_Position, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex3), (GLvoid*)offsetof(Vertex3, Position));
                    glEnableVertexAttribArray(Attribute_Position);

                    /* Setup texcoords. */
                    if(texCoordCount)
                    {
                         glVertexAttribPointer(Attribute_Texture0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex3), (GLvoid*)offsetof(Vertex3, TexCoord));
                         glEnableVertexAttribArray(Attribute_Texture0);
                    }

                    /* Setup normals. */
                    if(normalCount)
                    {
                         glVertexAttribPointer(Attribute_Normal, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex3), (GLvoid*)offsetof(Vertex3, Normal));
                         glEnableVertexAttribArray(Attribute_Normal);
                    }

                    /* Specify orientation. */
                    World.Identity();
                    World.Translate(Position);
                    World.Translate(Center);
                    World.RotateX(Rotation.X);
                    World.RotateY(Rotation.Y);
                    World.RotateZ(Rotation.Z);
                    World.Translate(-Center);
                    World.Scale(Scale);


                    for(std::deque<MaterialGroup>::iterator i = MatGroups.begin(); i < MatGroups.end(); i++)
                    {
                         /* Bind the group's material. */
                         i->Mat.Bind();

                         /* Draw the material's portion! */
                         glDrawArrays(GL_TRIANGLES, i->Start * 3, i->Faces * 3);

                         /* Unbind any material. */
                         i->Mat.Unbind();
                    }

                    /* Disable attributes. */
                    glDisableVertexAttribArray(Attribute_Position);
                    glDisableVertexAttribArray(Attribute_Texture0);
                    glDisableVertexAttribArray(Attribute_Normal);
               }
          }

     } /* Namespace Content. */
} /* Namespace LOGL_GLib. */
